/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package tp_sd_server.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import tp_sd_base.IServer;
import tp_sd_base.Set;
import tp_sd_base.User;
import tp_sd_server.gui.ServerGUI;

/**
 *
 * @author 
 */
public class ServerChat implements IServer {

    static {
        graph = new MapGraph<User, Boolean>();
        users = new ConcurrentHashMap<String, User>();
        connectedUsers = new ConcurrentHashMap<String, ClientThread>();
    }
    private ExecutorService service;
    private static ServerChat instance = new ServerChat();
    private static MapGraph<User, Boolean> graph;
    private static ConcurrentHashMap<String, User> users;
    private static ConcurrentHashMap<String, ClientThread> connectedUsers;
    private static int maxServerThread = 3;
    private static int SERVER_PORT = 12345;
    private volatile static MaxTreeSet<User> topUsers;
    private volatile static MaxTreeSet<User> topSeguidos;

    public static ServerChat getInstance() {
        return instance;
    }

    public static void setMaxServerThread(int maxServerThread) {
        ServerChat.maxServerThread = maxServerThread;
    }

    private static void addUser(User u) {
        users.put(u.getNickName(), u);
        graph.add(u);
    }

    private static void removeUser(User user) {
        users.remove(user.getNickName());
        graph.remove(user);
    }

    private static final Comparator<User> COMPARATOR = new Comparator<User>() {

        public int compare(User o1, User o2) {
            int i = o2.getMessageNum() - o1.getMessageNum();
            return i == 0 ? -1 : (i < 0 ? -1 : 1);
        }
    };

    public static void refreshTop(){
        int n= users.size();
        MaxTreeSet<User> maxTreeSet = new MaxTreeSet<User>(n, COMPARATOR);
        maxTreeSet.addAll(users.values());
        topUsers=maxTreeSet;
        initTopUser(4000);
    }

     public static void refreshTopFollowers(){
        int n= users.size();
        MaxTreeSet<User> maxTreeSet = new MaxTreeSet<User>(n, COMPARATOR_FOLLOWED);
        maxTreeSet.addAll(users.values());
        topSeguidos=maxTreeSet;
        
        initTopFollowers(4000);
    }

    private static List<Set<String,Integer>> subList(MaxTreeSet<User> set,int max,boolean messaging){
        List<Set<String, Integer>> list = new ArrayList<Set<String, Integer>>(max);
        Iterator<User> iterable = set.descendingIterator();
        int i=0;
        while (iterable.hasNext() && i<max) {
            User user = iterable.next();
            list.add(0, new Set<String, Integer>(user.getNickName(), messaging?user.getMessageNum():user.getNSeguidores()));
            i++;
        }
        return list;
    }

    public static List<Set<String, Integer>> getTopUsers(int n) {
        return subList(topUsers, n,true);

    }
    private static final Comparator<User> COMPARATOR_FOLLOWED = new Comparator<User>() {

        public int compare(User o1, User o2) {
            int i = o2.getNSeguidores() - o1.getNSeguidores();
            return i == 0 ? -1 : (i < 0 ? -1 : 1);
        }
    };

    public static List<Set<String, Integer>> getTopSeguidos(int n) {
        return subList(topSeguidos, n,false);

//        int max;
//        ArrayList<String> list = new ArrayList<String>();
//        Iterator<User> it = topSeguidos.descendingIterator();
//
//        if (n < 1) {
//            max = 10;
//        } else {
//            max = n;
//        }
//
//        for (int i = 0; i < max && it.hasNext(); i++) {
//            User u = it.next();
//            list.add(u.getNickName());
//        }
//        return list;
    }

//    public void initiateTimerTask(TimerTask t, long delay, long period) {
//        ServerTimerTask task = new ServerTimerTask(this);
//        tasks = new Timer(true);
//        tasks.schedule(t, delay, period);
//    }
    public static synchronized void addConnectedUser(String user, ClientThread thread) {
        if (user != null) {
            MessageLog.getInstance().addMessage(connectedUsers.toString());
            connectedUsers.put(user, thread);
            MessageLog.getInstance().addMessage("Conectou: " + user);

            ServerGUI gui = ServerGUI.getInstance();
            gui.addUser(user);

            List<String> list = new ArrayList<String>(1);
            list.add(user);

            for (ClientThread thread1 : connectedUsers.values()) {
                if (!thread1.equals(thread)) {
                    try {
                        thread1.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.ONLINE.ordinal(), IServer.OPERATION.ADD.ordinal(), null, null, list);
                    } catch (IOException ex) {
                        thread1.disconnect();
                    }
                }
            }
            MessageLog.getInstance().addMessage(connectedUsers.toString());
            
        }
    }

    public static synchronized  void removeConnectedUser(String user) {
        if (user != null) {
            connectedUsers.remove(user);
            MessageLog.getInstance().addMessage("Desligou: " + user);

            ServerGUI gui = ServerGUI.getInstance();
            gui.removeUser(user);

            List<String> list = new ArrayList<String>(1);
            list.add(user);

            for (ClientThread thread1 : connectedUsers.values()) {
                try {
                    thread1.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.ONLINE.ordinal(), IServer.OPERATION.REMOVE.ordinal(), null, null, list);
                } catch (IOException ex) {
                    thread1.disconnect();
                }
            }
        }
    }

    public static List<String> findAllOnline() {
        return new ArrayList<String>(connectedUsers.keySet());
    }

    public static List<String> findAllFollowers(String login) {
        List<String> list = new ArrayList<String>();
        for (User user : graph.getFollowers(users.get(login))) {
            list.add(user.getNickName());
        }
        return list;
    }

    public static List<String> findAllFollowed(String login) {
        List<String> list = new ArrayList<String>();
        for (User user : graph.getFollowed(users.get(login))) {
            list.add(user.getNickName());
        }
        return list;
    }

    public synchronized static boolean login(String login, String pass, ClientThread thread) {
        pass=md5(pass);
        if (users.containsKey(login) && users.get(login).getPass().equals(pass)) {
            if (connectedUsers.containsKey(login)) {
                try {
                    thread.sendError(ERROR_TYPE.DISCONNECT, "User already connected");
                } catch (IOException ex) {
                    thread.disconnect();
                }
                return false;
            } else {
                addConnectedUser(login, thread);
            }
            return true;
        }
        return false;

    }


public static String md5(String input){

        String res = "";
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(input.getBytes());
            byte[] md5 = algorithm.digest();
            String tmp = "";
            for (int i = 0; i < md5.length; i++) {
                tmp = (Integer.toHexString(0xFF & md5[i]));
                if (tmp.length() == 1) {
                    res += "0" + tmp;
                } else {
                   res += tmp;
                }
            }

        } catch (NoSuchAlgorithmException ex) {}

        return res;

    }



    public static boolean register(String login, String pass, ClientThread thread) {
        pass=md5(pass);
        if (users.containsKey(login)) {
            MessageLog.getInstance().addMessage("Registo falhado: " + login);
            return false;
        }
        User user = new User(login, pass);
        users.put(login, user);
        graph.add(user);
        addConnectedUser(login, thread);
        return true;
    }

    public static User removeUser(String login) {
        if (login != null) {
            removeConnectedUser(login);
            User user = users.remove(login);
            if (user != null) {
                graph.remove(user);
                topUsers.remove(user);
                topSeguidos.remove(user);
            }
            return user;
        }
        return null;
    }

    public static void addAssociationUser(String login, List<String> login2) {
        User user = users.get(login);
        login2.remove(login);
        graph.add(user, findUserByLogin(login2, Boolean.TRUE), Boolean.TRUE);

        List<String> list = new ArrayList<String>(1);
        list.add(login);
        try {
            ClientThread ct = connectedUsers.get(login);
            if (ct != null) {
                ct.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWED.ordinal(), IServer.OPERATION.ADD.ordinal(), null, null, login2);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }


        for (String string : login2) {
            if (string != null) {
                ClientThread thread = connectedUsers.get(string);
                if (thread != null) {
                    try {
                        thread.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWERS.ordinal(), IServer.OPERATION.ADD.ordinal(), null, null, list);
                    } catch (IOException ex) {
                        thread.disconnect();
                    } catch (NullPointerException ex) {
                    }
                }
            }

        }

    }

    public static void removeAssociationUser(String login, String login2) {
        User user = users.get(login);
        User user1 = users.get(login2);
        if (user != null) {
            graph.remove(user, user1);
            user1.decNSeguidores();
            //actualizaTopSeguidos(user1);
            List<String> list = new ArrayList<String>(1);
            list.add(login);
            ClientThread clientThread = null;
            try {
                clientThread = connectedUsers.get(login);
                clientThread.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWED.ordinal(), IServer.OPERATION.REMOVE.ordinal(), null, null, login2);
            } catch (IOException ex) {
                clientThread.disconnect();
            } catch (NullPointerException exception) {
            }



            ClientThread thread = connectedUsers.get(login2);
            if (thread != null) {
                try {
                    thread.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWERS.ordinal(), IServer.OPERATION.REMOVE.ordinal(), null, null, list);
                } catch (IOException ex) {
                    thread.disconnect();
                } catch (NullPointerException ex) {
                }
            }
        }
    }

    public static List<User> findUserByLogin(List<String> list, Boolean add) {
        List<User> users = new ArrayList<User>(list.size());
        for (String string : list) {
            if (string != null) {
                User user = ServerChat.users.get(string);
                if (user != null) {
                    if (add != null) {
                        if (add) {
                            user.incNSeguidores();
                        } else {
                            user.decNSeguidores();
                        }
                    }
                    users.add(user);
                }
            }
        }
        return users;
    }

    public static void removeAssociationUser(String login, List<String> login2) {
        User user = users.get(login);
        if (user != null) {
            for (User user1 : findUserByLogin(login2, Boolean.FALSE)) {
                graph.remove(user, user1);
                //actualizaTopSeguidos(user1);
                List<String> list = new ArrayList<String>(1);
                list.add(login);
                ClientThread clientThread = null;
                try {
                    clientThread = connectedUsers.get(login);
                    clientThread.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWED.ordinal(), IServer.OPERATION.REMOVE.ordinal(), null, null, login2);
                } catch (IOException ex) {
                    clientThread.disconnect();
                } catch (NullPointerException ex) {
                }


                for (String string : login2) {
                    if (string != null) {
                        ClientThread thread = connectedUsers.get(string);
                        if (thread != null) {
                            try {
                                thread.sendMessage(MESSAGE_TYPE.PARTIAL_REFRESH.ordinal(), IServer.REFRESH_TYPE.FOLLOWERS.ordinal(), IServer.OPERATION.REMOVE.ordinal(), null, null, list);
                            } catch (IOException ex) {
                                thread.disconnect();
                            } catch (NullPointerException ex) {
                            }
                        }
                    }
                }

            }
        }
    }

    public static void handlersMessages(String sender, String message) {
        User user = users.get(sender);
        user.incMessageNum();
        Collection<User> list = graph.getFollowers(user);
        for (User user1 : list) {
            ClientThread thread = connectedUsers.get(user1.getNickName());
            if (thread != null) {
                try {
                    thread.sendMessage(MESSAGE_TYPE.MESSAGE, user, message);
                } catch (IOException ex) {
                    thread.disconnect();
                }
            }
        }
    }

    public static void updateConfiguration(int port, int max_clients) {

        if (port <= 0) {
            port = 12345;
        }
        if (max_clients <= 0) {
            max_clients = 3;
        }

        SERVER_PORT = port;
        maxServerThread = max_clients;

    }
    private ServerSocket serverSocket;

    public ServerChat() {
//        for (int i = 0; i < 1000; i++) {
//            User u = new User("TESTE" + i, "TESTE" + i);
//            ServerChat.addUser(u);
//        }
    }

    public void disconnect() throws IOException {
        if (serverSocket != null && !serverSocket.isClosed()) {
            serverSocket.close();
        }
    }

    public void connect() throws IOException {
        String msg = "A criar o servidor. Porta: " + SERVER_PORT + " Max.Clientes: " + maxServerThread;
        MessageLog.getInstance().addMessage(msg);

        service = Executors.newFixedThreadPool(maxServerThread + 5);
        serverSocket = new ServerSocket(SERVER_PORT, maxServerThread);

        msg = "Servidor criado.";
        MessageLog.getInstance().addMessage(msg);

        initTopUser(300);
        initTopFollowers(300);
    }


    private static void initTopFollowers(int time){
        Timer timer= new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
               SwingWorker sw = new SwingWorker() {

                    @Override
                    protected Object doInBackground() throws Exception {
                        refreshTopFollowers();
                        return null;
                    }
                };
               sw.execute();
            }
        }, time);
    }


    private static void initTopUser(int time){
        Timer timer= new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
               SwingWorker sw = new SwingWorker() {

                    @Override
                    protected Object doInBackground() throws Exception {
                        refreshTop();
                        return null;
                    }
                };
               sw.execute();
            }
        }, time);
    }


    public void run() {
        SwingWorker doInBackground = new SwingWorker() {

            @Override
            protected Object doInBackground() throws Exception {
                serverSocket.setReuseAddress(false);

                while (!serverSocket.isClosed()) {
                    try {

                        String msg = "À espera de clientes";
                        MessageLog.getInstance().addMessage(msg);

                        Socket socket = serverSocket.accept();


                        System.out.println("Total utilizadores: " + users.size() + " Total connectados: " + connectedUsers.size());

                        handleNewClient(socket);
                    } catch (Exception e) {
                        
                    }
                }

                for(ClientThread thread : connectedUsers.values()){
                    try{
                            thread.disconnect();
                    }catch(Exception e){

                    }
                        }
                connectedUsers.clear();

                MessageLog.getInstance().addMessage("Loop terminado.");
                ServerGUI.getInstance().onDisconnect();
                return null;
            }
        };
        service.submit(doInBackground);
        doInBackground.execute();


        initTimer();
    }

    public synchronized static void setStop(boolean stop) {
        ServerChat.stop = stop;
    }




    protected void initTimer(){
        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                SwingWorker worker = new SwingWorker() {

                    @Override
                    protected Object doInBackground() throws Exception {
                        setStop(true);
                        saveFile("data.dat");
                        
                        initTimer();
                        setStop(false);
                        return null;

                        
                    }
                };
                service.submit(worker);
                worker.execute();
            }
        }, 4000);
    }

    protected void handleNewClient(Socket sock) {
        final ClientThread clientThread = new ClientThread(sock);



        if (connectedUsers.size() >= maxServerThread) {
            try {

                clientThread.sendError(ERROR_TYPE.DISCONNECT, "Número máximo de utilizadores atingido");
                clientThread.disconnect();

            } catch (IOException exc) {

                exc.printStackTrace();
            }
        } else {
            SwingWorker worker = new SwingWorker() {

                @Override
                protected Object doInBackground() throws Exception {

                    clientThread.run();
                    return null;
                }

                @Override
                protected void done() {
                    super.done();
                }
            };
            service.submit(worker);
            worker.execute();
        }
    }

    public volatile  static boolean stop=false;

    public synchronized static void saveFile(String name) throws IOException {
        
        FileOutputStream output = new FileOutputStream(name);
        ObjectOutputStream outputStream = new ObjectOutputStream(output);

                 
         outputStream.writeObject(users);
         outputStream.writeObject(graph);

         outputStream.flush();
         outputStream.close();

         output.flush();
         output.close();
        
    }

    public static void openFile(String name) throws FileNotFoundException, IOException, ClassNotFoundException{
        File file = new File(name);

        ObjectInputStream stream = new ObjectInputStream(new FileInputStream(file));
        users = (ConcurrentHashMap<String, User>) stream.readObject();
        graph = (MapGraph<User, Boolean>) stream.readObject();
        
        
        stream.close();

    }
}
